﻿using System;
using System.Configuration;
using System.IO;
using MLSharp.Classification;
using MLSharp.Utilities;

namespace MLSharp.Cpn
{
	/// <summary>
	/// Factory that creates <see cref="CounterPropagationNetwork"/>s.
	/// </summary>
	public class CounterPropagationNetworkFactory : IClassifierFactory
	{
		#region Private Static Fields

		/// <summary>
		/// The logger for the type.
		/// </summary>
		private static readonly log4net.ILog mLogger =
			log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

		#endregion

		#region Private Fields

		/// <summary>
		/// The path to CPN.exe
		/// </summary>
		private readonly string mCpnPath;

		/// <summary>
		/// The path to the working directory to use for temporary files.
		/// </summary>
		private readonly string mWorkingDirectory;

		#endregion

		#region Properties

		/// <summary>
		/// The options object for the factory.
		/// </summary>
		/// <remarks>
		/// Implementors should implement this property explicitly and expose 
		/// a strongly-typed options object instead.  
		/// </remarks>
		object IClassifierFactory.Options
		{
			get { return Options; }
		}

		/// <summary>
		/// Gets or sets the options to use.
		/// </summary>
		public CpnNeuralNetOptions Options { get; private set; }

		#endregion

		#region Public Constructors

		/// <summary>
		/// Creates a new factory and uses configuration settings from the App.config
		/// file.
		/// </summary>
		/// <remarks>
		/// You can configureCPN using the app.config file.  You can specify:
		/// <list>
		///		<item>CpnPath - the full path to CPN.exe.</item>
		///		<item>CpnWorkingDirectory - A directory to place intermediate output into.</item>
		/// </list>
		/// </remarks>
		public CounterPropagationNetworkFactory()
		{
			mCpnPath = ConfigurationManager.AppSettings["CpnPath"] ?? Path.GetFullPath("Cpn.exe");
			mWorkingDirectory = ConfigurationManager.AppSettings["CpnWorkingDirectory"] ?? Path.GetFullPath(@".\Cpn");

			Initialize();
		}

		/// <summary>
		/// Creates a factory using the specified settings.
		/// </summary>
		/// <param name="cpnPath">The path to cpn.exe.</param>
		/// <param name="workingDirectory">The working directory to use for temporary files.</param>
		public CounterPropagationNetworkFactory(string cpnPath, string workingDirectory)
		{
			mCpnPath = Path.GetFullPath(cpnPath);
			mWorkingDirectory = Path.GetFullPath(workingDirectory);

			Initialize();
		}

		#endregion

		#region Private Static Methods

		/// <summary>
		/// Writes the training commands for a level of the network.
		/// </summary>
		/// <param name="writer"></param>
		/// <param name="startingEta"></param>
		/// <param name="endingEta"></param>
		/// <param name="step"></param>
		/// <param name="iterationsPerEta"></param>
		/// <param name="lowerLevel">True to write the training commands for the lower-level network, false
		/// to write training commands for the upper level.</param>
		private static void WriteTraining(StreamWriter writer, double startingEta, double endingEta, double step, int iterationsPerEta, bool lowerLevel)
		{
			double etaDifference = startingEta - endingEta;
			//The result of dividing the difference by step should always be negative, so it has
			//to be factored out.
			int trainingPasses = -1 * (int)(etaDifference / step);

			double currentEta = startingEta;

			string etaFormat = lowerLevel ? "e 0 {0}" : "e {0} 0";

			for (int i = 0; i < trainingPasses; i++)
			{
				writer.WriteLine(etaFormat, currentEta);
				writer.WriteLine("r {0} {0}", iterationsPerEta);

				currentEta += step;
			}
		}

		#endregion

		#region Private Methods

		/// <summary>
		/// Performs common initialization.
		/// </summary>
		private void Initialize()
		{
			Options = new CpnNeuralNetOptions();

			if (!Directory.Exists(mWorkingDirectory))
			{
				Directory.CreateDirectory(mWorkingDirectory);
			}

			ValidateConfiguration();
		}

		/// <summary>
		/// Verifies that fields have been set to correct, valid values.
		/// </summary>
		private void ValidateConfiguration()
		{
			//Make sure the CPN executable path is valid.
			if (!File.Exists(mCpnPath))
			{
				throw new FileNotFoundException(string.Format("Unable to locate cpn.exe at '{0}'", mCpnPath));
			}
		}

		/// <summary>
		/// Gets the number of neurons to use in the middle layer.
		/// </summary>
		/// <returns></returns>
		private int GetNeuronCount(int independentVariableCount)
		{
			return Options.NumberOfNeurons == 0 ? independentVariableCount*2 : Options.NumberOfNeurons;
		}

		#endregion

		#region Public Methods

		/// <summary>
		/// Builds a classifier to predict the specified attribute using the 
		/// specified training data.
		/// </summary>
		/// <param name="trainingData">The training data.</param>
		/// <returns>A classifier.</returns>
		IClassifier IClassifierFactory.BuildClassifier(IDataSet trainingData)
		{
			return BuildNeuralNetwork(trainingData);
		}

		/// <summary>
		/// Builds a neural network from the specified training data.
		/// </summary>
		/// <param name="trainingData">The training data.</param>
		/// <returns>A neural net classifier.</returns>
		public CounterPropagationNetwork BuildNeuralNetwork(IDataSet trainingData)
		{
			//Verify that the options are valid.
			Options.Validate();

			//TEMP FILES
			//This is the actual CPN script that will be generated.
			string cpnScriptName = Path.Combine(mWorkingDirectory, NameGenerator.GetRandomName(10) + ".cpn");
			//And these are the training vectors
			string cpnInputTableName = Path.Combine(mWorkingDirectory, NameGenerator.GetRandomName(10) + ".training");
			//And this is where the weights will be written.
			string weightsFile = Path.Combine(mWorkingDirectory, NameGenerator.GetRandomName(10) + ".weights");

			//The number of independent attributes that are in the data set.
			int independentAttributeCount = trainingData.Attributes.Count - 1;

			//Write the training data.
			CpnDataWriter.WriteTrainingData(trainingData, cpnInputTableName);

			//This will store the command to create/recreate the network.
			string createNetworkCommand;

			//Write a cpn script to train the network
			using (StreamWriter writer = File.CreateText(cpnScriptName))
			{
				//Turn off paging
				writer.WriteLine("f P 0");

				//Create a network (note: right now, it only supports 1 target variable)
				createNetworkCommand = string.Format("m {0} {1} 1", independentAttributeCount,
				                                     GetNeuronCount(independentAttributeCount));

				writer.WriteLine(createNetworkCommand);

				//Read in the training data
				writer.WriteLine("rt {0}", cpnInputTableName);

				//Seed the random number generator
				writer.WriteLine("s {0}", Options.Seed);

				//Initialize the network with random weights between [-1,1]
				writer.WriteLine("ci");

				//Traing the lower level weights
				WriteTraining(writer, Options.LowerLevelStartingEta, Options.LowerLevelEndingEta, Options.LowerLevelStep,
				              Options.LowerLevelIterationsPerEta, true);

				//Train the upper level weights
				WriteTraining(writer, Options.UpperLevelStartingEta, Options.UpperLevelEndingEta, Options.UpperLevelStep,
							  Options.UpperLevelIterationsPerEta, false);

				//Dump the weights
				writer.WriteLine("sw {0}", weightsFile);

				//Exit the program
				writer.WriteLine("q");
			}

			//Run CPN
			ProcessOutput output = ProcessWrapper.Run(mCpnPath, mWorkingDirectory, "\"" + cpnScriptName + "\"");

			//Grab and echo any relevant messages
			if (mLogger.IsDebugEnabled)
			{
				mLogger.Debug("Dumping output from CPN.exe...");

				foreach (string line in output.StandardOutput.Split(new[] {"\r\n"}, StringSplitOptions.RemoveEmptyEntries))
				{
					if (!line.StartsWith("seed") && !line.StartsWith("running"))
					{
						mLogger.Debug(line);
					}
				}

				mLogger.Debug("--------------------");
			}

			//Grab the weights from the temp file
			string weights = File.ReadAllText(weightsFile);

			//Clean up temp files.
			File.Delete(cpnScriptName);
			File.Delete(cpnInputTableName);
			File.Delete(weightsFile);

			//Create a network that stores the network size and the weights
			return new CounterPropagationNetwork(createNetworkCommand, weights, mCpnPath, mWorkingDirectory);
		}

		#endregion
	}
}
